﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Affinity.Synthesis.ExpressionCells;

namespace Affinity.Synthesis
{
    /// <summary>
    /// The untyped base class for Synthesis, defining abstract methods and providing typed factory methods.
    /// </summary>
    public abstract class LambdaBinding
    {
        /// <summary>
        /// Creates a typed LambdaBinding for an expression taking one parameter (the binding target object) of type TParameter and returning a value of type TReturn.
        /// </summary>
        /// <typeparam name="TParameter"></typeparam>
        /// <typeparam name="TReturn"></typeparam>
        /// <param name="lambdaExpression"></param>
        /// <returns></returns>
        public static LambdaBinding<TReturn> Create<TParameter, TReturn>(Expression<Func<TParameter, TReturn>> lambdaExpression)
        {
            return new LambdaBinding<TParameter, TReturn>(lambdaExpression);
        }

        /// <summary>
        /// Creates a typed LambdaBinding for a LambdaExpression taking a single parameter (the binding target object) and returning a value of type TReturn.
        /// </summary>
        /// <typeparam name="TReturn"></typeparam>
        /// <param name="lambdaExpression"></param>
        /// <returns></returns>
        public static LambdaBinding<TReturn> Create<TReturn>(LambdaExpression lambdaExpression)
        {
            return (LambdaBinding<TReturn>)Activator.CreateInstance(typeof(LambdaBinding<,>).MakeGenericType(lambdaExpression.Parameters[0].Type, typeof(TReturn)), lambdaExpression);
        }

        /// <summary>
        /// Gets the current value of this LambdaBinding for the given target object and assures that when the value changes, 
        /// a property change notification will be fired.
        /// </summary>
        /// <param name="target"></param>
        /// <param name="propertyName"></param>
        /// <param name="defaultValue"></param>
        /// <param name="forceUseOfGetterAndSetter"></param>
        /// <returns></returns>
        public abstract object BindUntyped(object target, string propertyName, object defaultValue, bool forceUseOfGetterAndSetter = false);

        /// <summary>
        /// If a LambdaBinding is present for the specified target, triggers a refresh of the values in the corresponding tree of
        /// ExpressionCells and a property change notification for the output of that tree. 
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        public abstract bool TryPushValue(object target);

        /// <summary>
        /// Stops tracking changes for the specified binding target and property name.
        /// </summary>
        /// <param name="target"></param>
        /// <param name="propertyName"></param>
        public abstract void StopTracking(object target, string propertyName);  
    }

    /// <summary>
    /// LambdaBinding class with an explicit return type.
    /// </summary>
    /// <typeparam name="TReturn"></typeparam>
    public abstract class LambdaBinding<TReturn> : LambdaBinding
    {
        /// <summary>
        /// Gets the current value of this LambdaBinding for the given target object and assures that when the value changes, 
        /// a property change notification will be fired.
        /// </summary>
        /// <param name="target"></param>
        /// <param name="propertyName"></param>
        /// <param name="defaultValue"></param>
        /// <param name="forceUseOfGetterAndSetter"></param>
        /// <returns></returns>
        public abstract TReturn Bind(object target, string propertyName, TReturn defaultValue = default(TReturn), bool forceUseOfGetterAndSetter = false);

        /// <summary>
        /// Gets the current value of this LambdaBinding for the given target object and assures that when the value changes, 
        /// a property change notification will be fired.
        /// </summary>
        /// <param name="target"></param>
        /// <param name="propertyName"></param>
        /// <param name="defaultValue"></param>
        /// <param name="forceUseOfGetterAndSetter"></param>
        /// <returns></returns>
        public override sealed object BindUntyped(object target, string propertyName, object defaultValue, bool forceUseOfGetterAndSetter = false)
        {
            return Bind(target, propertyName, (TReturn)defaultValue, forceUseOfGetterAndSetter);
        }
    }

    /// <summary>
    /// LambdaBinding class with explicit parameter and return types.
    /// </summary>
    /// <typeparam name="TParameter"></typeparam>
    /// <typeparam name="TReturn"></typeparam>
    internal class LambdaBinding<TParameter, TReturn> : LambdaBinding<TReturn>
    {
        /// <summary>
        /// Constructor accepting the expression that this LambdaBinding will represent.
        /// </summary>
        /// <param name="lambdaExpression"></param>
        public LambdaBinding(Expression<Func<TParameter, TReturn>> lambdaExpression)
        {
            _ExpressionCell = new ExpressionCellularizer().Cellularize(lambdaExpression);
        }

        /// <summary>
        /// Constructor accepting the LambdaExpression that this LambdaBinding will represent.
        /// </summary>
        /// <param name="lambdaExpression"></param>
        public LambdaBinding(LambdaExpression lambdaExpression)
        {
            _ExpressionCell = new ExpressionCellularizer().Cellularize(lambdaExpression);
        }

        /// <summary>
        /// Gets the current value of this LambdaBinding for the given target object and assures that when the value changes, 
        /// a property change notification will be fired.
        /// </summary>
        /// <param name="target"></param>
        /// <param name="propertyName"></param>
        /// <param name="defaultValue"></param>
        /// <param name="forceUseOfGetterAndSetter"></param>
        /// <returns></returns>
        public override TReturn Bind(object target, string propertyName, TReturn defaultValue, bool forceUseOfGetterAndSetter)
        {
            IExpressionCell expressionCell = null;
            
            WeakReference weakReference = new WeakReference(target);

            if (!_Instances.TryGetValue(target, out expressionCell))
            {
                expressionCell = Instantiate();

                if (!forceUseOfGetterAndSetter && target is IRaiseNotifyPropertyChanged)
                {
                    Action changedHandler = null;

                    changedHandler = () =>
                    {
                        object weakReferenceTarget = weakReference.Target;

                        if (weakReferenceTarget == null)
                        {
                            expressionCell.Output.Changed -= changedHandler;
                        }
                        else ((IRaiseNotifyPropertyChanged)weakReferenceTarget).RaisePropertyChanged(propertyName);
                    };

                    expressionCell.Output.Changed += changedHandler;
                }
                else
                {
                    PropertyInfo property = target.GetType().GetProperty(propertyName);
                    if (property == null) throw new ArgumentException("Property not found on target.");
                    if (property.GetSetMethod() == null) throw new ArgumentException("Property is not publicly settable.");
                    
                    Action changedHandler = null;
                    
                    changedHandler = () => 
                    {
                        object weakReferenceTarget = weakReference.Target;

                        if (weakReferenceTarget == null)
                        {
                            expressionCell.Output.Changed -= changedHandler;
                        }
                        else if (expressionCell.Output.HasValue) property.SetValue(weakReferenceTarget, expressionCell.Output.Value, null); 
                    };

                    expressionCell.Output.Changed += changedHandler;
                }

                // Must set this before setting the cell in motion, or else an infinite recursion may result.
                _Instances.Add(target, expressionCell);

                // Set the cell in motion by providing the single input value (our target).
                ExpressionCellValue inputValue = new ExpressionCellValue(null);
                inputValue.Set(true, target);
                expressionCell.Inputs = new ExpressionCellValue[] { inputValue };
            }

            return expressionCell.Output.HasValue ? (TReturn)expressionCell.Output.Value : defaultValue;
        }

        /// <summary>
        /// If a LambdaBinding is present for the specified target, triggers a refresh of the values in the corresponding tree of
        /// ExpressionCells and a property change notification for the output of that tree. 
        /// </summary>
        /// <param name="target"></param>
        /// <returns></returns>
        public override bool TryPushValue(object target)
        {
            bool success = false;

            IExpressionCell expressionCell = null;
            
            int hashCode = target.GetHashCode();

            if (_Instances.TryGetValue(hashCode, out expressionCell))
            {
                if (expressionCell.Inputs.Length > 0) expressionCell.Inputs[0].RaiseChanged();
                if (expressionCell.Output.HasValue) expressionCell.Output.RaiseChanged();
                success = true;
            }

            return success;
        }

        /// <summary>
        /// Stops tracking changes for the specified binding target and property name.
        /// </summary>
        /// <param name="target"></param>
        /// <param name="propertyName"></param>
        public override void StopTracking(object target, string propertyName)
        {
            IExpressionCell expressionCell = null;
            
            int hashCode = target.GetHashCode();

            if (_Instances.TryGetValue(hashCode, out expressionCell))
            {
                _Instances.Remove(hashCode);
                expressionCell.Output.ClearChangedHandlers();
            }
        }

        private IExpressionCell Instantiate()
        {
            return _ExpressionCell.Copy();
        }

        private IExpressionCell _ExpressionCell = null;
        private ConditionalWeakTable<object, IExpressionCell> _Instances = new ConditionalWeakTable<object, IExpressionCell>();
    }
}